package org.hackystat.devcathlon.match;

import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.Map.Entry;

import javax.xml.datatype.XMLGregorianCalendar;

import org.hackystat.dailyprojectdata.client.DailyProjectDataClient;
import org.hackystat.devcathlon.configuration.ConfigurationException;
import org.hackystat.devcathlon.configuration.ConfigurationManager;
import org.hackystat.devcathlon.configuration.jaxb.DevcathlonConfiguration;
import org.hackystat.devcathlon.event.Event;
import org.hackystat.devcathlon.team.Team;
import org.hackystat.devcathlon.user.User;
import org.hackystat.sensorbase.client.SensorBaseClient;
import org.hackystat.sensorbase.resource.projects.jaxb.Project;
import org.hackystat.utilities.tstamp.Tstamp;

/**
 * Provides information about a single match.
 * Matches are managed (created, retrieved, etc.) via the MatchManager. 
 * @author Philip Johnson
 */
public class Match {
  /** The start of the match. */
  private XMLGregorianCalendar startTime;
  /** The end of the match. */
  private XMLGregorianCalendar endTime;
  /** The (team, project) pairs for this match. */
  private Map<Team, Project> teams2projects;
  /** The list of Events for this match. */
  private List<Event> events;
  /** The unique name for this match. */
  private String name;
  /** Maps teams to their SensorBaseClient instances. */
  private Map<Team, SensorBaseClient> teams2sensorbases = new HashMap<Team, SensorBaseClient>();
  /** Maps teams to their DailyProjectDataClient instances. */
  private Map<Team, DailyProjectDataClient> teams2dailyprojectdatas = 
    new HashMap<Team, DailyProjectDataClient>();
  
  /**
   * Clients must use the MatchManager to create Match instances.
   * Performs validation as follows:
   * <ul>
   * <li> There must be at least one team in this match.
   * <li> The events list must be non-empty.
   * <li> Every team must have a project.
   * <li> Every team must have at least one member. 
   * <li> The startTime and endTime must be within the Project interval. 
   * <li> It must be possible to create SensorBase and DPD clients for each team. 
   * </ul> 
   * @param name The name of the Match. 
   * @param startTime The start time. 
   * @param endTime The end time. 
   * @param teams2projects A set of tuples indicating the teams in this match and their Projects.
   * @param events The set of events in which the teams compete in this match.
   * @throws MatchException If validation fails. 
   */
  Match(String name, XMLGregorianCalendar startTime, XMLGregorianCalendar endTime, 
      Map<Team, Project> teams2projects, List<Event> events) throws MatchException {
    this.name = name;
    this.startTime = startTime;
    this.endTime = endTime;
    this.teams2projects = teams2projects;
    this.events = events;
    validateEvents();
    validateTeamProjects();
    setupHackystatClients();
  }
  
  /**
   * Checks to make sure that the list of events for this match is non-empty.
   * @throws MatchException If there are no defined events for this match. 
   */
  private void validateEvents() throws MatchException {
    if (this.events.isEmpty()) {
      throw new MatchException(String.format("Match %s has no defined events.", this.name));
    }
  }
    
  /**
   * Checks the validity of projects.  This means:
   * <ul>
   * <li> Every team has a Project instance. 
   * <li> Every member of the team is also a project member (but not necessarily vice-versa).
   * <li> The project start/end dates are greater than the match start/end dates.
   * </ul>  
   * @throws MatchException If the projects are not valid.  
   */
  private void validateTeamProjects() throws MatchException {
    // Make sure this match contains at least one team. 
    if ((this.teams2projects == null) || (this.teams2projects.keySet().isEmpty())) {
      throw new MatchException(String.format("Match %s has no teams.", this.name)); 
    }
    for (Entry<Team, Project> entry : this.teams2projects.entrySet()) {
      Team team = entry.getKey();
      Project project = entry.getValue();
      // Make sure each team has a project. 
      if (project == null) {
        throw new MatchException(String.format("Match %s has no project.", this.name));
      }
      for (User user : team.getMembers()) {
        String userEmail = user.getEmail();
        // Make sure everyone in the team is a project member or owner. 
        if (!project.isMemberOrOwner(userEmail)) {
          String msg = String.format("Match %s has team %s with user %s not in project %s", 
              this.name, team.getName(), userEmail, project.getName());
          throw new MatchException(msg);
        }
      }
      // Make sure that the match falls within the project start/end times. 
      if (Tstamp.lessThan(this.startTime, project.getStartTime())) {
        String msg = String.format("Match %s has start time %s before project start time %s.", 
            this.name, this.startTime, project.getStartTime());
        throw new MatchException(msg);
      }
      if (Tstamp.greaterThan(this.endTime, project.getEndTime())) {
        String msg = String.format("Match %s has end time %s after project start time %s.", 
            this.name, this.endTime, project.getEndTime());
        throw new MatchException(msg);
      }
    }
  }
  
  /**
   * Sets up the SensorBase and DailyProjectData clients for each team. 
   * @throws MatchException If the clients cannot be created and authenticated against the
   * running services. 
   */
  private void setupHackystatClients() throws MatchException {
    // First, get the SensorBase, DPD, and Telemetry hosts from the Configuration. 
    ConfigurationManager configManager = ConfigurationManager.getInstance();
    DevcathlonConfiguration config;
    try {
      config = configManager.getConfiguration();
    }
    catch (ConfigurationException e) {
      throw new MatchException("Error getting configuration", e);
    }
    String sensorbase = config.getHackystat().getSensorbase();
    String dailyprojectdata = config.getHackystat().getDailyprojectdata();
    
    // Now, for each team, create the associated client instances. 
    for (Team team : this.teams2projects.keySet()) {
      // Each team must have at least one user!  
      if (team.getMembers().isEmpty()) {
        throw new MatchException(String.format("Team %s has no users.", team));
      }
      // Any team user must have legal credentials for the client. 
      User user = (User) team.getMembers().toArray()[0];
      String email = user.getEmail();
      String password = user.getPassword();
      
      // Make the SensorBaseClient for this team, validating the credentials. 
      SensorBaseClient sensorBaseClient = new SensorBaseClient(sensorbase, email, password);
      try {
        sensorBaseClient.authenticate();
      }
      catch (Exception e) {
        throw new MatchException(String.format("Could not authenticate %s with %s", 
            email, sensorbase), e);
      }
      this.teams2sensorbases.put(team, sensorBaseClient);
      
      // Make the DailyProjectDataClient for this team, validating the credentials. 
      DailyProjectDataClient dpdClient = 
        new DailyProjectDataClient(dailyprojectdata, email, password);
      try {
        dpdClient.authenticate();
      }
      catch (Exception e) {
        throw new MatchException(String.format("Could not authenticate %s with %s", 
            email, dailyprojectdata), e);
      }
      this.teams2dailyprojectdatas.put(team, dpdClient);
      
      // Don't do the telemetry client until we start needing it. This speeds up testing. 
      /*
      TelemetryClient telemetryClient = 
        new TelemetryClient(telemetry, email, password);
      try {
        telemetryClient.authenticate();
      }
      catch (Exception e) {
        throw new MatchException(String.format("Could not authenticate %s with %s", 
            email, telemetry), e);
      }
      this.teams2telemetrys.put(team, telemetryClient);
      */
    }
  }
  
  /**
   * Returns the SensorBaseClient for this team.
   * As long as the passed team is a member of this match, this should reliably return the client.
   * @param team The team. 
   * @return Their associated SensorBaseClient instance. 
   */
  public SensorBaseClient getSensorBaseClient(Team team) {
    return this.teams2sensorbases.get(team);
  }
  
  /**
   * Returns the DailyProjectDataClient for this team.
   * As long as the passed team is a member of this match, this should reliably return the client.
   * @param team The team. 
   * @return Their associated DailyProjectDataClient instance. 
   */
  public DailyProjectDataClient getDailyProjectDataClient(Team team) {
    return this.teams2dailyprojectdatas.get(team);
  }
  
  /**
   * Return the start date for this Match.
   * @return The start date. 
   */
  public XMLGregorianCalendar getStartTime() {
    return this.startTime;
  }

  /**
   * Return the end date for this Match. 
   * @return The end date. 
   */
  public XMLGregorianCalendar getEndTime() {
    return this.endTime;
  }

  /**
   * Return the set of teams in this Match. 
   * @return The teams in this Match. 
   */
  public Set<Team> getTeams() {
    return this.teams2projects.keySet();
  }
  
  /**
   * Return the Project associated with the given Team for this match.
   * As long as the team is part of this match, then a project should be returned. 
   * @param team The team whose project is to be retrieved.
   * @return The project. 
   */
  public Project getProject(Team team) {
    return this.teams2projects.get(team);
  }

  /**
   * Return the list of Events for this Match. 
   * Careful!  Do not modify this list. 
   * @return The list of Events in this match.
   */
  public List<Event> getEvents() {
    return this.events;
  }
  
  /**
   * Return the (unique) name of this match. 
   * @return The match name. 
   */
  public String getName() {
    return this.name;
  }
}
